/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.observation.Event;
import offset.nodes.Constants;

/**
 * A PathConfiguratorFinder based on HashMaps.
 *
 * @author Walter L�tgenau
 */
public class HashMapPathConfiguratorFinder implements PathConfiguratorFinder, NodeTypeObserver {
    /**
     * Represents a path object and some useful operations.
     */
    class Path {
        String[] components;
        String path;

        public Path(String path) {
            components = path.split(Constants.PATH_SEPARATOR);
            this.path = path;
        }

        /**
         * Return the path as a string.
         *
         * @return the path
         */
        public String getPath() {
            return path;
        }

        /**
         * Return the depth of the path represented by this object.
         *
         * @return path depth
         */
        public int getDepth() {
            return components.length - 1;
        }

        /**
         * Return the sub path of depth as a string. If the depth is equal or greater
         * than the depth of the path object, return the whole path as a string.
         *
         * @param depth the depth
         * @return the sub path
         */
        public String getSubpath(int depth) {
            if (depth >= components.length - 1)
                return path;

            return createSubpath(depth);
        }

        /**
         * Return the path component at depth
         *
         * @param depth the depth
         * @return the path component
         */
        public String getComponent(int depth) {
            if (depth > components.length - 1)
                return "";
            return components[depth];
        }

        /**
         * Create a subpath of the specified depth as a string.
         *
         * @param depth the depth of the subpath
         * @return the subpath
         */
        protected String createSubpath(int depth) {
            StringBuilder buf = new StringBuilder();
            for (int i = 0; i <= depth; i++) {
                if (i > 0)
                    buf.append(Constants.PATH_SEPARATOR);
                buf.append(components[i]);
            }

            return buf.toString();
        }
    }

    /**
     * A group of nodes of the same depth.
     */
    class DepthConfigurators {

        int depth;
        HashMap<String, List<Node>> configurators = new HashMap<String, List<Node>>();

        /**
         * The depth of the node group.
         *
         * @param depth the depth
         */
        public DepthConfigurators(int depth) {
            this.depth = depth;
        }

        /**
         * Add a node to the node group.
         *
         * @param configurator
         * @throws RepositoryException
         */
        public void addConfigurator(Node configurator) throws RepositoryException {
            if (configurator.getDepth() - 1 != getDepth())
                return;
            List<Node> pathConfigurators = this.configurators.get(configurator.getParent().getPath());
            if (pathConfigurators == null)
                pathConfigurators = new ArrayList<Node>();
            pathConfigurators.add(configurator);
            this.configurators.put(configurator.getParent().getPath(), pathConfigurators);
        }

        /**
         * Return the size of the group.
         *
         * @return the size
         */
        public int size() {
            return configurators.size();
        }

        /**
         * Return the node associated to path. If the
         * path is deeper than the node group, only the first
         * part of the path with a depth of the node group will be used
         * for the search.
         *
         * @param path The path to search
         * @return the node found
         */
        public List<Node> getConfigurators(Path path) {
            // configurator group is deeper
            if (depth > path.getDepth())
                return null;

            List<Node> pathConfigurators = configurators.get(path.getSubpath(depth));
            if (pathConfigurators == null)
                return null;
            return pathConfigurators;
        }

        public int getDepth() {
            return depth;
        }
    }

    /**
     * A matcher, that accepts all nodes
     */
    class UnconditionalMatcher implements NodeMatcher {

        @Override
        public boolean matches(Node node) {
            return true;
        }

    }

    /** The list of node groups of the same depth, sorted in descending depth */
    List<DepthConfigurators> configurators = new ArrayList<DepthConfigurators>();
    /** The node type managed by the finder */
    String nodeType;
    /** The matcher, that checks the configurators */
    NodeMatcher matcher;

    /**
     * Create a finder for a new node type.
     *
     * @param nodeType The node type
     * @throws RepositoryException
     */
    public HashMapPathConfiguratorFinder(String nodeType) throws RepositoryException {
        init(nodeType, new UnconditionalMatcher());
    }

   /**
     * Create a finder for a new node type.
     *
     * @param nodeType The node type
     * @throws RepositoryException
     */
    public HashMapPathConfiguratorFinder(String nodeType, NodeMatcher matcher) throws RepositoryException {
        init(nodeType, matcher);
    }

   /**
     * Initialize a finder for a new node type and matcher.
     *
     * @param nodeType The node type
     * @throws RepositoryException
     */
    protected void init(String nodeType, NodeMatcher matcher) throws RepositoryException {
        this.nodeType = nodeType;
        this.matcher = matcher;
        NodeTypeStartupExtension.getInstance().addNodeTypeObserver(nodeType, this);
    }

    /** {@inheritdoc}*/
    @Override
    public Node[] findPathConfigurators(String path) {
        LinkedList<Node> result = new LinkedList<Node>();

        Path searchPath = new Path(path);
        for (DepthConfigurators depthConfigurators : configurators) {
            if (depthConfigurators.getDepth() > searchPath.getDepth())
                return result.toArray(new Node[result.size()]);

            List<Node> pathConfigurators = depthConfigurators.getConfigurators(searchPath);
            if (pathConfigurators != null) {
                Iterator<Node> pc = pathConfigurators.iterator();
                while (pc.hasNext()) {
                    Node configurator = pc.next();

                    try {
                        configurator.getPath();
                    } catch (RepositoryException e) {
                        pc.remove(); // no longer valid, has been removed
                        continue;
                    }

                    result.add(configurator);
                }
            }
        }

        return result.toArray(new Node[result.size()]);
    }

    /** {@inheritdoc}*/
    @Override
    public void nodeAdded(Node node) throws RepositoryException {
        if (!matcher.matches(node))
            return;

        addConfigurator(node);
    }

    /** {@inheritdoc}*/
    @Override
    public void nodeRemoved(String path) throws RepositoryException {
        Path searchPath = new Path(path);
        for (DepthConfigurators depthConfigurators : configurators) {
            if (depthConfigurators.getDepth() > searchPath.getDepth() - 1)
                return;

            if (depthConfigurators.getDepth() == searchPath.getDepth() - 1) {
                List<Node> pathConfigurators = depthConfigurators.getConfigurators(searchPath);
                if (pathConfigurators == null)
                    return;
                
                Iterator<Node> pc = pathConfigurators.iterator();
                while (pc.hasNext()) {
                    Node configurator = pc.next();

                    try {
                        if (configurator.getName().equals(searchPath.getComponent(searchPath.getDepth())))
                            pc.remove();
                    } catch (RepositoryException e) {
                        pc.remove(); // no longer valid, has been removed
                        continue;
                    }
                }

            }
        }

    }

    /**
     * Add the given node to the PathConfiguratorFinder configuration.
     *
     * Search the groups for a group of the correct depth. If found, add
     * the node, if not, create a new group, add the node to it and insert
     * the group.
     *
     * The groups are ordered by ascending depth to have the most recent
     * configurators returned at the end of the list by findPathConfigurator(s).
     *
     * @param configurator the node to add.
     * @throws RepositoryException
     */
    public void addConfigurator(Node configurator) throws RepositoryException {
        Node parent = configurator.getParent();
        ListIterator<DepthConfigurators> dc = configurators.listIterator();

        while (dc.hasNext()) {
            DepthConfigurators depthConfigurators = dc.next();

            // element of higher depth, insert new group before element
            if (depthConfigurators.getDepth() > parent.getDepth()) {
                DepthConfigurators newDepth = new DepthConfigurators(parent.getDepth());
                newDepth.addConfigurator(configurator);
                configurators.add(dc.previousIndex(), newDepth);
                return;
            }
            // element of the same depth. Add element to existing group
            else if(depthConfigurators.getDepth() == parent.getDepth()) {
                // add and return
                depthConfigurators.addConfigurator(configurator);
                return;
            }
            // element of lower depth
            else {
                continue;
            }
        }

        // new element at list end. Add element to new group
        DepthConfigurators newDepth = new DepthConfigurators(parent.getDepth());
        newDepth.addConfigurator(configurator);
        // insert new group and return
        configurators.add(newDepth);
    }

}
